homework 5, version 0
Submission by: Ian Weaver (hahvard@mit.edu)
Table of Contents
xxxxxxxxxxTableOfContents(depth=10)Homework 5: Epidemic modeling II
18.S191, fall 2020
This notebook contains built-in, live answer checks! In some exercises you will see a coloured box, which runs a test case on your code, and provides feedback based on the result. Simply edit the code, run it, and the check runs again.
For MIT students: there will also be some additional (secret) test cases that will be run as part of the grading process, and we will look at your notebook and write comments.
Feel free to ask questions!
"Ian Weaver"
"hahvard"
Let's create a package environment:
xxxxxxxxxxmd"_Let's create a package environment:_"xxxxxxxxxxbegin using Pkg Pkg.activate(mktempdir())endxxxxxxxxxxbegin Pkg.add(["PlutoUI", "Plots"]) using Plots gr() theme(:dark) using PlutoUIendIn this problem set, we will look at a simple spatial agent-based epidemic model: agents can interact only with other agents that are nearby. (In the previous homework any agent could interact with any other, which is not realistic.)
A simple approach is to use discrete space: each agent lives in one cell of a square grid. For simplicity we will allow no more than one agent in each cell, but this requires some care to design the rules of the model to respect this.
We will adapt some functionality from the previous homework. You should copy and paste your code from that homework into this notebook.
Exercise 1: Wandering at random in 2D
In this exercise we will implement a random walk on a 2D lattice (grid). At each time step, a walker jumps to a neighbouring position at random (i.e. chosen with uniform probability from the available adjacent positions).
Exercise 1.1
We define a struct type Coordinate that contains integers x and y.
xxxxxxxxxxstruct Coordinate{T <: Int} x::T y::Tend👉 Construct a Coordinate located at the origin.
0
0
xxxxxxxxxxorigin = Coordinate(0, 0)Got it!
Keep it up!
👉 Write a function make_tuple that takes an object of type Coordinate and returns the corresponding tuple (x, y). Boring, but useful later!
make_tuple (generic function with 1 method)xxxxxxxxxxfunction make_tuple(c::Coordinate) (c.x, c.y)endGot it!
You got the right answer!
Exercise 1.2
In Julia, operations like + and * are just functions, and they are treated like any other function in the language. The only special property you can use the infix notation: you can write
1 + 2
instead of
+(1, 2)
(There are lots of special 'infixable' function names that you can use for your own functions!)
When you call it with the prefix notation, it becomes clear that it really is 'just another function', with lots of predefined methods.
3xxxxxxxxxx+(1, 2)+ (generic function with 199 methods)xxxxxxxxxx+Extending + in the wild
Because it is a function, we can add our own methods to it! This feature is super useful in general languages like Julia and Python, because it lets you use familiar syntax (
a + b*c) on objects that are not necessarily numbers!One example we've see before is the
RGBtype in Homework 1. You are able to do:0.5 * RGB(0.1, 0.7, 0.6)to multiply each color channel by
. This is possible because Images.jlwrote a method:*(::Real, ::AbstractRGB)::AbstractRGB
👉 Implement addition on two Coordinate structs by adding a method to Base.:+
xxxxxxxxxxBase.:+(a::Coordinate, b::Coordinate) = Coordinate(a.x+b.x, a.y+b.y)13
14
xxxxxxxxxxCoordinate(3,4) + Coordinate(10,10) # uncomment to check + worksPluto has some trouble here, you need to manually re-run the cell above!
Got it!
Yay ❤
Exercise 1.3
In our model, agents will be able to walk in 4 directions: up, down, left and right. We can define these directions as Coordinates.
1
0
0
1
-1
0
0
-1
xxxxxxxxxxpossible_moves = [ Coordinate( 1, 0), Coordinate( 0, 1), Coordinate(-1, 0), Coordinate( 0,-1),]👉 rand(possible_moves) gives a random possible move. Add this to the coordinate Coordinate(4,5) and see that it moves to a valid neighbor.
5
5
xxxxxxxxxxCoordinate(4, 5) + rand(possible_moves)We are able to make a Coordinate perform one random step, by adding a move to it. Great!
👉 Write a function trajectory that calculates a trajectory of a Wanderer w when performing n steps., i.e. the sequence of positions that the walker finds itself in.
Possible steps:
Use
rand(possible_moves, n)to generate a vector ofnrandom moves. Each possible move will be equally likely.To compute the trajectory you can use either of the following two approaches:
🆒 Use the function
accumulate(see the live docs foraccumulate). Use+as the function passed toaccumulateand thewas the starting value (initkeyword argument).Use a
forloop calling+.
trajectory (generic function with 1 method)xxxxxxxxxxfunction trajectory(w::Coordinate, n::Int) moves = rand(possible_moves, n) traj = [Coordinate(0, 0) for _ in 1:n] accumulate!(+, traj, moves, init=w)end11
15
20
26
xxxxxxxxxxlet x = [1, 4, 5, 6] y = [0, 0, 0, 0] accumulate!(+, y, x, init=10)end3
4
3
5
4
5
4
4
5
4
6
4
6
3
6
4
5
4
5
5
4
5
5
5
5
4
4
4
3
4
4
4
4
3
5
3
6
3
6
2
6
1
7
1
7
2
7
3
6
3
6
4
6
3
7
3
6
3
7
3
xxxxxxxxxxtest_trajectory = trajectory(Coordinate(4,4), 30) # uncomment to testGot it!
Awesome!
plot_trajectory! (generic function with 1 method)Exercise 1.4
👉 Plot 10 trajectories of length 1000 on a single figure, all starting at the origin. Use the function plot_trajectory! as demonstrated above.
Remember from last week that you can compose plots like this:
let
# Create a new plot with aspect ratio 1:1
p = plot(ratio=1)
plot_trajectory!(p, test_trajectory) # plot one trajectory
plot_trajectory!(p, another_trajectory) # plot the second one
...
p
end
xxxxxxxxxxlet trajectories = [trajectory(Coordinate(0, 0), 1_000) for _ in 1:10] p = plot(ratio=1) for trajectory in trajectories plot_trajectory!(p, trajectory) end pendExercise 1.5
Agents live in a box of side length
One relatively simple boundary condition is a collision boundary:
Each wall of the box is a wall, modelled using "collision": if the walker tries to jump beyond the wall, it ends up at the position inside the box that is closest to the goal.
👉 Write a function collide_boundary which takes a Coordinate c and a size c. This is similar to extend_mat from Homework 1.
collide_boundary (generic function with 1 method)xxxxxxxxxxfunction collide_boundary(c::Coordinate, L::Number) new_x = clamp(c.x, -L, L) new_y = clamp(c.y, -L, L) Coordinate(new_x, new_y)end10
4
xxxxxxxxxxcollide_boundary(Coordinate(12,4), 10) # uncomment to testExercise 1.6
👉 Implement a 3-argument method of trajectory where the third argument is a size. The trajectory returned should be within the boundary (use collide_boundary from above). You can still use accumulate with an anonymous function that makes a move and then reflects the resulting coordinate, or use a for loop.
trajectory (generic function with 2 methods)xxxxxxxxxxfunction trajectory(c::Coordinate, n::Int, L::Number) moves = rand(possible_moves, n) traj = [Coordinate(0, 0) for _ in 1:n] current_location = c collide = x -> collide_boundary(x, L) for i in 1:n traj[i] = collide(current_location) current_location += moves[i] end return trajendtrajectory2 (generic function with 1 method)xxxxxxxxxxfunction trajectory2(c::Coordinate, n::Int, L::Number) traj = accumulate(1:n; init=c) do p, _ collide_boundary(p + rand(possible_moves), L) end return pushfirst!(traj, c)endxxxxxxxxxxlet long_trajectory = trajectory(Coordinate(4,4), 10, 10) p = plot(ratio=1) plot_trajectory!(p, long_trajectory, color=:cyan) scatter!(p, [4], [4]) pend557.897 ns (32 allocations: 1.41 KiB)
xxxxxxxxxxwith_terminal() do start = Coordinate(4,4) trajectory($start, 10, 10)end576.151 ns (32 allocations: 1.50 KiB)
xxxxxxxxxxwith_terminal() do start = Coordinate(4,4) trajectory2($start, 10, 10)endxxxxxxxxxxusing BenchmarkToolsExercise 2: Wanderering Agents
In this exercise we will create Agents which have a location as well as some infection state information.
Let's define a type Agent. Agent contains a position (of type Coordinate), as well as a status of type InfectionStatus (as in Homework 4).)
(For simplicity we will not use a num_infected field, but feel free to do so!)
xxxxxxxxxx InfectionStatus S I Rxxxxxxxxxxmutable struct Agent <: AbstractAgent position::Coordinate status::InfectionStatusend-2
-2
S::InfectionStatus = 0
xxxxxxxxxxAgent(Coordinate(rand(-2:2, 2)...), S)Exercise 2.1
👉 Write a function initialize that takes parameters
It returns a Vector of N randomly generated Agents. Their coordinates are randomly sampled in the
initialize (generic function with 1 method)xxxxxxxxxxfunction initialize(N::Number, L::Number) agents = [Agent(Coordinate(rand(-L:L, 2)...), S) for _ in 1:N] agents[rand(1:N)].status = I return agentsend-2
-10
S::InfectionStatus = 0
-10
7
I::InfectionStatus = 1
-9
7
S::InfectionStatus = 0
xxxxxxxxxxinitialize(3, 10)Got it!
Fantastic!
color (generic function with 1 method)xxxxxxxxxx# Color based on infection statuscolor(s::InfectionStatus) = if s == S "#1f78b4"elseif s == I "#c44e52"else "#029e73"endposition (generic function with 1 method)xxxxxxxxxxposition(a::AbstractAgent) = a.position # uncomment this linecolor (generic function with 2 methods)xxxxxxxxxxcolor(a::AbstractAgent) = color(a.status) # uncomment this lineExercise 2.2
👉 Write a function visualize that takes in a collection of agents as argument, and the box size L. It should plot a point for each agent at its location, coloured according to its status.
You can use the keyword argument c=color.(agents) inside your call to the plotting function make the point colors correspond to the infection statuses. Don't forget to use ratio=1.
visualize (generic function with 1 method)xxxxxxxxxxfunction visualize(agents::Vector, L) p = scatter() for agent in agents scatter!( p, (position(agent).x, position(agent).y), legend = false, c = color(agent), msw = 0, xlim = (-L, L), ylim = (-L, L), ratio = 1, ) end return pendxxxxxxxxxxlet N = 20 L = 10 visualize(initialize(N, L), L) # uncomment this line!endExercise 3: Spatial epidemic model – Dynamics
Last week we wrote a function interact! that takes two agents, agent and source, and an infection of type InfectionRecovery, which models the interaction between two agent, and possibly modifies agent with a new status.
This week, we define a new infection type, CollisionInfectionRecovery, and a new method that is the same as last week, except it only infects agent if agents.position==source.position.
xxxxxxxxxxabstract type AbstractInfection endxxxxxxxxxxstruct CollisionInfectionRecovery <: AbstractInfection p_infection::Float64 p_recovery::Float64endWrite a function interact! that takes two Agents and a CollisionInfectionRecovery, and:
If the agents are at the same spot, causes a susceptible agent to communicate the desease from an infectious one with the correct probability.
if the first agent is infectious, it recovers with some probability
bernoulli (generic function with 1 method)xxxxxxxxxxbernoulli(p::Number) = rand() < pis_infected (generic function with 1 method)xxxxxxxxxxis_infected(agent::AbstractAgent) = agent.status == Iis_susceptible (generic function with 1 method)xxxxxxxxxxis_susceptible(agent::AbstractAgent) = agent.status == Sset_status! (generic function with 1 method)xxxxxxxxxxset_status!(agent::AbstractAgent, new_status::InfectionStatus) = agent.status = new_statusinteract! (generic function with 1 method)xxxxxxxxxxfunction interact!( agent::AbstractAgent, source::AbstractAgent, infection::CollisionInfectionRecovery) if is_infected(agent) && bernoulli(infection.p_recovery) set_status!(agent, R) end if position(agent) == position(source) && is_susceptible(agent) && is_infected(source) && bernoulli(infection.p_infection) set_status!(agent, I) endendExercise 3.1
Your turn!
👉 Write a function step! that takes a vector of Agents, a box size L and an infection. This that does one step of the dynamics on a vector of agents.
Choose an Agent
sourceat random.Move the
sourceone step, and usecollide_boundaryto ensure that our agent stays within the box.For all other agents, call
interact!(other_agent, source, infection).return the array
agentsagain.
complementary (generic function with 1 method)xxxxxxxxxxfunction complementary(x::Array) idx_chosen = rand(1:length(x)) x[idx_chosen], x[1:end .!= idx_chosen]endmove! (generic function with 1 method)xxxxxxxxxxfunction move!(agent::AbstractAgent, L::Number) agent.position = collide_boundary(agent.position + rand(possible_moves), L)endstep! (generic function with 1 method)xxxxxxxxxxfunction step!(agents::Vector, L::Number, infection::AbstractInfection) source, other_agents = complementary(agents) move!(source, L) for other_agent in other_agents interact!(other_agent, source, infection) end return agentsendExercise 3.2
If we call step! N times, then every agent will have made one step, on average. Let's call this one sweep of the simulation.
👉 Create a before-and-after plot of
Initialize a new vector of agents (
N=50,L=40,infectionis given aspandemicbelow).Plot the state using
visualize, and save the plot as a variableplot_before.Run
k_sweepssweeps.Plot the state again, and store as
plot_after.Combine the two plots into a single figure using
plot(plot_before, plot_after)
0.5
1.0e-5
xxxxxxxxxx k_sweeps Slider(1:10000, default=1000, show_value=true)xxxxxxxxxxlet N = 50 L = 40 agents = initialize(N, L) plot_before = visualize(agents, L) for _ in 1:k_sweeps*N step!(agents, L, pandemic) end plot_after = visualize(agents, L) plot(plot_before, plot_after, link = :all)endExercise 3.3
Every time that you move the slider, a completely new simulation is created an run. This makes it hard to view the progress of a single simulation over time. So in this exercise, we we look at a single simulation, and plot the S, I and R curves.
👉 Plot the SIR curves of a single simulation, with the same parameters as in the previous exercise. Use k_sweep_max = 10000 as the total number of sweeps.
10000xxxxxxxxxxk_sweep_max = 10000xxxxxxxxxxlet N = 50 L = 10 agents = initialize(N, L) # compute k_sweep_max number of sweeps and plot the SIR Ss, Is, Rs = Int[], Int[], Int[] for i in 1:k_sweeps*N step!(agents, L, pandemic) push!(Ss, sum(is_susceptible.(agents))) push!(Is, sum(is_infected.(agents))) push!(Rs, N - (Ss[i] + Is[i])) end p = plot(xguide="step", yguide="number of agents") plot!(p, Ss, label='S', c=color(S)) plot!(p, Is, label='I', c=color(I)) plot!(p, Rs, label='R', c=color(R))endHint
After every sweep, count the values
Exercise 3.4 (optional)
Let's make our plot come alive! There are two options to make our visualization dynamic:
👉1️⃣ Precompute one simulation run and save its intermediate states using deepcopy. You can then write an interactive visualization that shows both the state at time visualize) and the history of
👉2️⃣ Use @gif from Plots.jl to turn a sequence of plots into an animation. Be careful to skip about 50 sweeps between each animation frame, otherwise the GIF becomes too large.
This an optional exercise, and our solution to 2️⃣ is given below.
xxxxxxxxxxlet N = 50 L = 25 k_sweeps = 50 x = initialize(N, L) # initialize to empty arrays Ss, Is, Rs = Int[], Int[], Int[] Tmax = 200 for t in 1:Tmax # Do a sweep for i in 1:k_sweeps*N step!(x, L, pandemic) end S_current = sum(is_susceptible.(x)) I_current = sum(is_infected.(x)) R_current = N - (S_current + I_current) push!(Ss, S_current) push!(Is, I_current) push!(Rs, R_current) #... track S, I, R in Ss Is and Rs # Visualize sweep left = visualize(x, L) right = plot(xlim=(1,Tmax), ylim=(1,N), size=(600,300)) plot!(right, 1:t, Ss, color=color(S), label="S") plot!(right, 1:t, Is, color=color(I), label="I") plot!(right, 1:t, Rs, color=color(R), label="R") plot(left, right) endend"/tmp/jl_zoercf"
"000001.png"
"000002.png"
"000003.png"
"000004.png"
"000005.png"
"000006.png"
"000007.png"
"000008.png"
"000009.png"
"000010.png"
"000011.png"
"000012.png"
"000013.png"
"000014.png"
"000015.png"
"000016.png"
"000017.png"
"000018.png"
"000019.png"
"000020.png"
"000042.png"
"000043.png"
"000044.png"
"000045.png"
"000046.png"
"000047.png"
"000048.png"
"000049.png"
"000050.png"
"000051.png"
xxxxxxxxxxbegin p = plot(1) anim = for x=0:0.1:5 push!(p, 1, sin(x)) endendHint
let
N = 50
L = 40
x = initialize(N, L)
# initialize to empty arrays
Ss, Is, Rs = Int[], Int[], Int[]
Tmax = 200
for t in 1:Tmax
for i in 1:50N
step!(x, L, pandemic)
end
#... track S, I, R in Ss Is and Rs
left = visualize(x, L)
right = plot(xlim=(1,Tmax), ylim=(1,N), size=(600,300))
plot!(right, 1:t, Ss, color=color(S), label="S")
plot!(right, 1:t, Is, color=color(I), label="I")
plot!(right, 1:t, Rs, color=color(R), label="R")
plot(left, right)
end
end
Exercise 3.5
👉 Using
0.91
1.0e-9
xxxxxxxxxxcauses_outbreak = CollisionInfectionRecovery(0.91, 1e-9)0.91
1.0e-5
xxxxxxxxxxdoes_not_cause_outbreak = CollisionInfectionRecovery(0.91, 1e-5)xxxxxxxxxxlet N = 100 L = 20 k_sweeps = 50 T_max = 100 agents = initialize(N, L) # compute k_sweep_max number of sweeps and plot the SIR Ss, Is, Rs = Int[], Int[], Int[] for t in 1:T_max for i in 1:k_sweeps*N step!(agents, L, does_not_cause_outbreak) end S_current = sum(is_susceptible.(agents)) I_current = sum(is_infected.(agents)) R_current = N - (S_current + I_current) push!(Ss, S_current) push!(Is, I_current) push!(Rs, R_current) end p = plot(xguide="step", yguide="number of agents") plot!(p, Ss, label='S', c=color(S)) plot!(p, Is, label='I', c=color(I)) plot!(p, Rs, label='R', c=color(R))endxxxxxxxxxx# let # N = 100# L = 20# k_sweeps = 50# T_max = 100# infection = does_not_cause_outbreak # SIRs, agents = simulation(100, 20, 50, 100, infection) # p = plot(xguide="step", yguide="number of agents")# plot!(p, SIRs.S, label='S', c=color(S))# plot!(p, SIRs.I, label='I', c=color(I))# plot!(p, SIRs.R, label='R', c=color(R))# end Exercise 3.6
👉 With the parameters of Exercise 3.2, run 50 simulations. Plot p_infection and p_recovery values from last week. Why?
Gonna wrap this into a function
xxxxxxxxxxusing Statistics: stdsimulation (generic function with 1 method)xxxxxxxxxxfunction simulation( N::Int, L::Int, k_sweeps::Int, T_max::Int, infection::AbstractInfection,) agents = initialize(N, L) Ss, Is, Rs = Int[], Int[], Int[] for i in 1:T_max for i in 1:k_sweeps*N step!(agents, L, infection) end S_current = sum(is_susceptible.(agents)) I_current = sum(is_infected.(agents)) R_current = N - (S_current + I_current) push!(Ss, S_current) push!(Is, I_current) push!(Rs, R_current) end return (S=Ss, I=Is, R=Rs), agentsendrepeat_simulations (generic function with 1 method)sir_mean_error_plot (generic function with 1 method)xxxxxxxxxxif run_3_6 let num_simulations = 50 N = 100 L = 40 k_sweeps = 50 T_max = 100 infection = pandemic simulations = repeat_simulations( num_simulations, N, L, k_sweeps, T_max, infection, ) sir_mean_error_plot(simulations) endendxxxxxxxxxxneed_different_parameters_because = md""""""Exercise 4: Effect of socialization
In this exercise we'll modify the simple mixing model. Instead of a constant mixing probability, i.e. a constant probability that any pair of people interact on a given day, we will have a variable probability associated with each agent, modelling the fact that some people are more or less social or contagious than others.
Exercise 4.1
We create a new agent type SocialAgent with fields position, status, num_infected, and social_score. The attribute social_score represents an agent's probability of interacting with any other agent in the population.
xxxxxxxxxxmutable struct SocialAgent <: AbstractAgent position::Coordinate status::InfectionStatus num_infected::Int social_score::Numberenddefine the position and color methods for SocialAgent as we did for Agent. This will allow the visualize function to work. on both kinds of Agents
color (generic function with 3 methods)xxxxxxxxxxbegin position(a::SocialAgent) = a.position color(a::SocialAgent) = color(a.status)end👉 Create a function initialize_social that takes N and L, and creates N agents within a 2L x 2L box, with social_scores chosen from 10 equally-spaced between 0.1 and 0.5. (see LinRange)
initialize_social (generic function with 1 method)xxxxxxxxxxfunction initialize_social(N::Number, L::Number) scores = LinRange(0.1, 0.5, 10) agents = [ SocialAgent(Coordinate(rand(-L:L, 2)...), S, 0, rand(scores)) for _ in 1:N] agents[rand(1:N)].status = I return agentsendNow that we have 2 agent types
let's create an AbstractAgent type
Go back in the notebook and make the agent types a subtype of AbstractAgent.
xxxxxxxxxxabstract type AbstractAgent endExercise 4.2
Not all two agents who end up in the same grid point may actually interact in an infectious way – they may just be passing by and do not create enough exposure for communicating the disease.
👉 Write a new interact! method on SocialAgent which adds together the social_scores for two agents and uses that as the probability that they interact in a risky way. Only if they interact in a risky way, the infection is transmitted with the usual probability.
interact! (generic function with 2 methods)xxxxxxxxxxfunction interact!( agent::SocialAgent, source::SocialAgent, infection::CollisionInfectionRecovery) if is_infected(agent) && bernoulli(infection.p_recovery) set_status!(agent, R) end if position(agent) == position(source) && bernoulli(agent.social_score + source.social_score) && is_susceptible(agent) && is_infected(source) && bernoulli(infection.p_infection) set_status!(agent, I) endendMake sure step!, position, color, work on the type SocialAgent. If step! takes an untyped first argument, it should work for both Agent and SocialAgent types without any changes. We actually only need to specialize interact! on SocialAgent.
Exercise 4.3
👉 Plot the SIR curves of the resulting simulation.
N = 50; L = 40; number of steps = 200
In each step call step! 50N times.
xxxxxxxxxxlet N = 50 L = 40 global social_agents = initialize_social(N, L) Ss, Is, Rs = [], [], [] Tmax = 20 for t in 1:Tmax # Do a sweep for i in 1:k_sweeps*N step!(social_agents, L, pandemic) end S_current = sum(is_susceptible.(social_agents)) I_current = sum(is_infected.(social_agents)) R_current = N - (S_current + I_current) push!(Ss, S_current) push!(Is, I_current) push!(Rs, R_current) #... track S, I, R in Ss Is and Rs # Visualize sweep left = visualize(social_agents, L) right = plot(xlim=(1,Tmax), ylim=(1,N), size=(600,300)) plot!(right, 1:t, Ss, color=color(S), label="S") plot!(right, 1:t, Is, color=color(I), label="I") plot!(right, 1:t, Rs, color=color(R), label="R") plot(left, right, size=(600, 300)) endendExercise 4.4
👉 Make a scatter plot showing each agent's social_score on one axis, and the num_infected from the simulation in the other axis. Run this simulation several times and comment on the results.
Enter cell code...
xxxxxxxxxx👉 Run a simulation for 100 steps, and then apply a "lockdown" where every agent's social score gets multiplied by 0.25, and then run a second simulation which runs on that same population from there. What do you notice? How does changing this factor form 0.25 to other numbers affect things?
Enter cell code...
xxxxxxxxxxExercise 5: (Optional) Effect of distancing
We can use a variant of the above model to investigate the effect of the mis-named "social distancing" (we want people to be socially close, but physically distant).
In this variant, we separate out the two effects "infection" and "movement": an infected agent chooses a neighbouring site, and if it finds a susceptible there then it infects it with probability
Separately, an agent chooses a neighbouring site to move to, and moves there with probability
When
👉 How does the disease spread in this case?
Enter cell code...
xxxxxxxxxx👉 Run the dynamics repeatedly, and plot the sites which become infected.
Enter cell code...
xxxxxxxxxx👉 How does this change as you increase the density
This is basically the site percolation model.
When we increase
Enter cell code...
xxxxxxxxxx👉 Investigate how this leaky quarantine affects the infection dynamics with different densities.
Enter cell code...
xxxxxxxxxxFunction library
Just some helper functions used in the notebook.
hint (generic function with 1 method)almost (generic function with 1 method)still_missing (generic function with 2 methods)keep_working (generic function with 2 methods)Fantastic!
Splendid!
Great!
Yay ❤
Great! 🎉
Well done!
Keep it up!
Good job!
Awesome!
You got the right answer!
Let's move on to the next section.
correct (generic function with 2 methods)not_defined (generic function with 1 method)